package io.kiki.stack.http.feign;

import io.kiki.stack.http.feign.Request.Options;

import java.io.IOException;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.Stream;

import static io.kiki.stack.http.feign.ExceptionPropagationPolicy.UNWRAP;
import static io.kiki.stack.http.feign.FeignException.errorExecuting;
import static io.kiki.stack.http.feign.Util.checkNotNull;

final class SynchronousMethodHandler implements MethodHandler {

    private final Client client;
    private final ResponseHandler responseHandler;
    private final MethodHandlerConfiguration methodHandlerConfiguration;

    private SynchronousMethodHandler(MethodHandlerConfiguration methodHandlerConfiguration, Client client, ResponseHandler responseHandler) {

        this.methodHandlerConfiguration = checkNotNull(methodHandlerConfiguration, "methodHandlerConfiguration");
        this.client = checkNotNull(client, "client for %s", methodHandlerConfiguration.getTarget());
        this.responseHandler = responseHandler;
    }

    @Override
    public Object invoke(Object[] argv) throws Throwable {
        RequestTemplate requestTemplate = methodHandlerConfiguration.getBuildTemplateFromArgs().create(argv);
        Options options = findOptions(argv);
        Retryer retryer = this.methodHandlerConfiguration.getRetryer().clone();
        while (true) {
            try {
                return executeAndDecode(requestTemplate, options);
            } catch (RetryableException e) {
                try {
                    retryer.continueOrPropagate(e);
                } catch (RetryableException th) {
                    Throwable cause = th.getCause();
                    if (methodHandlerConfiguration.getExceptionPropagationPolicy() == UNWRAP && cause != null) {
                        throw cause;
                    } else {
                        throw th;
                    }
                }
                if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
                    methodHandlerConfiguration.getLogger().logRetry(methodHandlerConfiguration.getMethodMetadata().configKey(), methodHandlerConfiguration.getLogLevel());
                }
                continue;
            }
        }
    }

    Object executeAndDecode(RequestTemplate requestTemplate, Options options) throws Throwable {
        Request request = targetRequest(requestTemplate);

        if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
            methodHandlerConfiguration.getLogger().logRequest(methodHandlerConfiguration.getMethodMetadata().configKey(), methodHandlerConfiguration.getLogLevel(), request);
        }

        Response response;
        long start = System.nanoTime();
        try {
            response = client.execute(request, options);
            // ensure the request is set. TODO: remove in Feign 12
            response = response.toBuilder().request(request).requestTemplate(requestTemplate).build();
        } catch (IOException e) {
            if (methodHandlerConfiguration.getLogLevel() != Logger.Level.NONE) {
                methodHandlerConfiguration.getLogger().logIOException(methodHandlerConfiguration.getMethodMetadata().configKey(), methodHandlerConfiguration.getLogLevel(), e, elapsedTime(start));
            }
            throw errorExecuting(request, e);
        }

        long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
        return responseHandler.handleResponse(methodHandlerConfiguration.getMethodMetadata().configKey(), response, methodHandlerConfiguration.getMethodMetadata().returnType(), elapsedTime);
    }

    long elapsedTime(long start) {
        return TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start);
    }

    Request targetRequest(RequestTemplate requestTemplate) {
        for (RequestInterceptor requestInterceptor : methodHandlerConfiguration.getRequestInterceptorList()) {
            requestInterceptor.apply(requestTemplate);
        }
        return methodHandlerConfiguration.getTarget().apply(requestTemplate);
    }

    Options findOptions(Object[] argv) {
        if (argv == null || argv.length == 0) {
            return this.methodHandlerConfiguration.getOptions().getMethodOptions(methodHandlerConfiguration.getMethodMetadata().method().getName());
        }
        return Stream.of(argv).filter(Options.class::isInstance).map(Options.class::cast).findFirst().orElse(this.methodHandlerConfiguration.getOptions().getMethodOptions(methodHandlerConfiguration.getMethodMetadata().method().getName()));
    }

    static class MethodHandlerFactory implements io.kiki.stack.http.feign.MethodHandlerFactory<Object> {

        private final Client client;
        private final Retryer retryer;
        private final List<RequestInterceptor> requestInterceptors;
        private final ResponseHandler responseHandler;
        private final Logger logger;
        private final Logger.Level logLevel;
        private final ExceptionPropagationPolicy propagationPolicy;
        private final RequestTemplateFactoryResolver requestTemplateFactoryResolver;
        private final Options options;

        MethodHandlerFactory(Client client, Retryer retryer, List<RequestInterceptor> requestInterceptors, ResponseHandler responseHandler, Logger logger, Logger.Level logLevel, ExceptionPropagationPolicy propagationPolicy, RequestTemplateFactoryResolver requestTemplateFactoryResolver, Options options) {
            this.client = checkNotNull(client, "client");
            this.retryer = checkNotNull(retryer, "retryer");
            this.requestInterceptors = checkNotNull(requestInterceptors, "requestInterceptors");
            this.responseHandler = checkNotNull(responseHandler, "responseHandler");
            this.logger = checkNotNull(logger, "logger");
            this.logLevel = checkNotNull(logLevel, "logLevel");
            this.propagationPolicy = propagationPolicy;
            this.requestTemplateFactoryResolver = checkNotNull(requestTemplateFactoryResolver, "requestTemplateFactoryResolver");
            this.options = checkNotNull(options, "options");
        }

        @Override
        public MethodHandler create(Target<?> target, MethodMetadata md, Object requestContext) {
            final RequestTemplateFactory buildTemplateFromArgs = requestTemplateFactoryResolver.resolve(target, md);
            MethodHandlerConfiguration methodHandlerConfiguration = new MethodHandlerConfiguration(md, target, retryer, requestInterceptors, logger, logLevel, buildTemplateFromArgs, options, propagationPolicy);
            return new SynchronousMethodHandler(methodHandlerConfiguration, client, responseHandler);
        }
    }
}
